/* 
 * Copyright (c) 2012, Fromentin Xavier, Schnell Michaël, Dervin Cyrielle, Brabant Quentin
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *      * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *      * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *      * The names of its contributors may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Fromentin Xavier, Schnell Michaël, Dervin Cyrielle OR Brabant Quentin 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package kameleon.gui.model;

import java.io.File;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.*;

import kameleon.exception.KameleonException;
import kameleon.exception.NotUniquePlugInInfoException;
import kameleon.exception.UnknownExtensionException;
import kameleon.gui.exception.InvalidOutputFileException;
import kameleon.gui.util.FileConstants;
import kameleon.gui.util.LanguageConstants;
import kameleon.plugin.PlugInInfo;
import kameleon.util.IOObject;
import kameleon.util.WorkSpaceManager;

/**
 * Model responsible for the handling of the files. Allows the user
 * to add new files, remove files from the history, save the most
 * recently used files from one session to another, set the analyzers
 * used by the files and the generators used for the next generation,
 * choose the file charset and view detailed information about the
 * files.
 * 
 * @author		Schnell Michaël
 * @version		1.0
 */
public class FileModel extends MessageModel
implements FileConstants, LanguageConstants {

	/**
	 * Number of files which are stored from session to session.
	 */
	public static final int HISTORY_SIZE = 10 ;

	/**
	 * Default format of the dates displayed in the graphical interface.
	 */
	public static final String DEFAULT_DATE_FORMAT = "dd/MM/yyyy HH:mm" ; //$NON-NLS-1$

	/**
	 * Formatter used to display the dates in the default format.
	 * 
	 * @see		#DEFAULT_DATE_FORMAT
	 */
	public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat(DEFAULT_DATE_FORMAT) ;

	/**
	 * Index of the currently selected file (or {@code null} if no file
	 * is selected)
	 */
	protected Integer currentFileIndex ;

	/**
	 * Index of the last file which was removed from the history.
	 */
	protected int deletedIndex ;

	/**
	 * History of the recently used files and their generations.
	 */
	protected List<FileInfo> recentFiles ;

	/**
	 * Target file for the generation (provided by the user).
	 */
	protected File outputFile ;

	/**
	 * Charset used to decode the analyzed files.
	 */
	protected String charset ;

	/**
	 * Flag indicating if a new file has just been added.
	 */
	protected boolean newFileAdded ;

	/**
	 * Flag indicating that the selected file in the 
	 * history has just changed.
	 */
	protected boolean selectionHasChanged ;

	/**
	 * Flag indicating that a file has just been removed.
	 */
	protected boolean fileRemoved ;

	/**
	 * Ids of the currently selected generators.
	 */
	protected List<String> selectedGenerators ;

	/**
	 * Builds an instance with the given options.
	 * 
	 * <p>Reads the history of the recently used files.
	 * 
	 * @param 	debugMode
	 * 			flag indicating whether the debug mode 
	 * 			should be activated ({@code true} means
	 * 			activated)
	 */
	public FileModel(boolean debugMode) {
		super(debugMode) ;
		this.currentFileIndex = null ;
		this.newFileAdded = false ;
		this.selectionHasChanged = false ;
		this.fileRemoved = false ;
		this.charset = Charset.defaultCharset().displayName() ;
		this.readHistory() ;
	}// FileModel(boolean)

	/**
	 * Builds an instance with default options.
	 */
	public FileModel() {
		this(DEFAULT_DEBUG_MODE) ;
	}// FileModel()

	/**
	 * Reads the list of recently used files from the history file.
	 */
	protected void readHistory() {
		try {
			File history = new File(CONFIG_HISTORY_FILE) ;
			if (history.exists()) {
				Object obj = IOObject.readObjectFromFile(
						CONFIG_HISTORY_FILE) ;
				if (obj instanceof List<?>) {
					this.recentFiles = (List<FileInfo>) obj ;
					return ;
				}// if
			}// if
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
			Message infoMsg = new InformationMessage(
					InformationMessage.State.INFORMATION,
					READING_HISTORY_ERROR) ;
			this.addMessage(infoMsg) ;
		}// try
		this.recentFiles = new LinkedList<FileInfo>() ;
	}// readHistory()

	/**
	 * Writes the list of recently used files to the history file.
	 */
	protected void writeHistory() {
		try {
			WorkSpaceManager.ensureWorkSpace() ;
			// We only keep the HISTORY_SIZE files inside history.
			List<FileInfo> subList = new LinkedList<FileInfo>() ;
			int maxI = Math.min(this.recentFiles.size(), HISTORY_SIZE) ;
			Iterator<FileInfo> iter = this.recentFiles.iterator() ;
			for(int i=0; i<maxI; ++i) {
				FileInfo element = iter.next() ;
				subList.add(element) ;
			}// for
			IOObject.writeObjectToFile(CONFIG_HISTORY_FILE, subList) ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
			Message infoMsg = new InformationMessage( 
					InformationMessage.State.INFORMATION,
					WRITING_HISTORY_ERROR) ;
			this.addMessage(infoMsg) ;
		}// try
	}// writeHistory()

	/**
	 * Indicates if a file is currently selected.
	 * 
	 * @return	{@code true} if a file is currently selected,
	 * 			{@code false} otherwise
	 */
	public boolean fileIsSelected() {
		return (this.currentFileIndex != null) ;
	}// fileIsSelected()

	/**
	 * Returns the index of the file with the given path
	 * in the history file list.
	 * 
	 * @param 	path
	 * 			absolute path of the requested file
	 * 
	 * @return	index of the requested file or {@code null}
	 * 			if the file was not found
	 */
	public Integer getPosition(String path) {
		int position = 0 ;
		boolean found = false ;
		Iterator<FileInfo> iter = this.recentFiles.iterator() ;
		while (iter.hasNext() && !found) {
			FileInfo fi = iter.next() ;
			found = fi.getPath().equals(path) ;
			++position ;
		}// while
		//TODO Review - possibly throw an exception here
		// The given path was not found
		if (!found) {
			return null ;
		}// if
		return new Integer(position-1) ;
	}// getPosition(String)

	/**
	 * Sets the index for the currently selected file.
	 * 
	 * @param 	position
	 * 			index of the selected file
	 */
	public void setCurrentFilePosition(int position) {
		this.currentFileIndex = new Integer(position) ;
	}// setCurrentFilePosition(int)

	/**
	 * Returns the index of the currently selected file.
	 * 
	 * @return	Index of the currently selected file or {@code -1}
	 * 			if no file is selected
	 */
	public int getCurrentFilePosition() {
		if (this.currentFileIndex == null) {
			return -1 ;
		}// if
		return this.currentFileIndex.intValue() ;
	}// getCurrentFilePosition()

	/**
	 * Returns a formatted {@code String} the last generation date 
	 * of the currently selected file.
	 * 
	 * @return	Last generation date of the currently selected
	 * 			file or {@code null} if the file was never generated
	 * 			or if there is no selected file
	 */
	public String getFileLastGenerationDate() {
		FileInfo current = this.getSelectedFileInfo() ;
		if (current != null) {
			Date last = current.getLastGeneration() ;
			if (last != null) {
				return DATE_FORMATTER.format(last) ;
			}// if
		}// if
		return null ;
	}// getFileLastGenerationDate()

	/**
	 * Returns the extension of the currently selected file.
	 * 
	 * @return	Extension of the currently selected file or
	 * 			{@code null} if no file is selected or {@code ""}
	 * 			if the file has no extension
	 */
	public String getFileType() {
		FileInfo current = this.getSelectedFileInfo() ;
		if (current != null) {
			String fileName = current.getName() ;
			int positionDot = fileName.lastIndexOf('.') ;
			if (positionDot != -1) {
				// Extract the extension and return it
				return fileName.substring(positionDot+1) ;
			}// if
			return "" ;//$NON-NLS-1$
		}// if
		return null ;
	}// getFileType()

	/**
	 * Returns an array with the information about the 
	 * recently used files.
	 * 
	 * @return	Array containing all the information about the 
	 * 			recently used files
	 */
	public FileInfo[] getRecentFileInfo() {
		return this.recentFiles.toArray(
				new FileInfo[this.recentFiles.size()]) ;
	}// getRecentFileInfo()

	/**
	 * Sets the output file for future generations.
	 * 
	 * @param	selectedFile
	 * 			new output file
	 * 
	 * @throws 	InvalidOutputFileException
	 * 			if an invalid file is provided
	 *///TODO Further test if the given file is valid
	public void setOutputFile(File selectedFile) 
			throws InvalidOutputFileException {
		if (selectedFile == null) {
			throw new InvalidOutputFileException() ;
		}// if
		this.outputFile = selectedFile ;
	}// setOutPutFile(File)

	/**
	 * Adds a new file. The model will add the file to history 
	 * and select it.
	 * 
	 * @param	newFile
	 * 			added file
	 */
	public void addFile(File newFile) {
		String extension = getExtension(newFile) ;
		FileInfo fi = new FileInfo(newFile.getAbsolutePath()) ;
		
		// Test if the file is already in the history
		if (this.recentFiles.contains(fi)) {
			// We simply selected the added file
			this.currentFileIndex = this.getPosition(fi.getPath()) ;
			this.selectionHasChanged = true ;
		} else {
			int insertIndex = 0 ;
			if (this.recentFiles.isEmpty()) {
				this.recentFiles.add(fi) ;
			} else {
				this.recentFiles.add(insertIndex, fi);
			}// if
			this.setCurrentFilePosition(insertIndex) ;
			try {
				if (this.am.getNAnalyzers(extension) > 0) {
					try {
						PlugInInfo analyzer = 
								this.am.getAnalyzerInfo(extension) ;
						fi.setIdFormat(analyzer.getId()) ;
					} catch (NotUniquePlugInInfoException ex) {
						fi.setIdFormat(null) ;
					}// try
				}// if
			} catch (UnknownExtensionException ex) {
				fi.setIdFormat(null);
			}// try
			this.writeHistory() ;
			this.selectionHasChanged = true ;
			this.newFileAdded = true ;
		}// if
		try {
			this.notifyObservers() ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
		this.selectionHasChanged = false ;
		this.newFileAdded = false ;
	}// addFile(File)

	/**
	 * Indicates if a new file has just been added.
	 * 
	 * @return	{@code true} if a new file has just been added,
	 * 			{@code false} otherwise
	 */
	public boolean newFileAdded() {
		return this.newFileAdded ;
	}// newFileAdded()

	/**
	 * Indicates if the current selection has just changed.
	 * 
	 * @return	{@code true} if the current selection has just changed,
	 * 			{@code false} otherwise
	 */
	public boolean selectionHasChanged() {
		return this.selectionHasChanged ;
	}// selectedHasChanged()

	/**
	 * Returns the information about the currently selected file.
	 * 
	 * @return	{@code PlugInInfo} for the current selection or
	 * 			{@code null} if no file is selected
	 */
	public FileInfo getSelectedFileInfo() {
		if(this.currentFileIndex != null) {
			return this.recentFiles.get(
					this.currentFileIndex.intValue()) ;
		}// if
		return null ;
	}// getSelectedFileInfo()

	/**
	 * Selects the file with the given path in the history.
	 * 
	 * @param	filePath
	 * 			absolute path the of the file to select
	 */
	public void selectFile(String filePath) {
		this.currentFileIndex = this.getPosition(filePath) ;
		this.selectionHasChanged = true ;
		try {
			this.notifyObservers() ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
		this.selectionHasChanged = false ;
	}// selectFile(String)

	/**
	 * Clears the current selection if there is one.
	 */
	public void clearSelection() {
		if (this.currentFileIndex != null) {
			this.currentFileIndex = null ;
			this.selectionHasChanged = true ;
			this.fileRemoved = false ;
			try {
				this.notifyObservers() ;
			} catch (KameleonException ke) {
				this.displayDebugInformation(ke) ;
			}// try
			this.selectionHasChanged = false ;
		}// if
	}// clearSelection()

	/**
	 * Sets the analyzer used for the currently selected file.
	 * 
	 * @param 	analyzerId
	 * 			id of the new analyzer for the currently selected file
	 *///TODO Review the case of no file selected
	public void setCurrentFileAnalyzerId(String analyzerId) {
		FileInfo current = this.getSelectedFileInfo() ;
		if (current != null) {
			current.setIdFormat(analyzerId) ;
		} // if
	}// setCurrentFileAnalyzerId(String)

	/**
	 *  Sets the analyzer used for the currently selected file
	 *  and updates the history file.
	 * 
	 * @param 	analyzer
	 * 			analyzer for the currently selected file
	 */
	public void setCurrentFileAnalyzer(PlugInInfo analyzer) {
		this.setCurrentFileAnalyzerId(analyzer.getId()) ;
		this.writeHistory() ;
		this.selectionHasChanged = true ;
		try {
			this.notifyObservers() ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
		this.selectionHasChanged = false ;
	}// setCurrentFileAnalyzer(PlugInInfo)

	/**
	 * Returns the informations about the analyzer for the currently
	 * selected file.
	 * 
	 * @return	instance of {@code PlugInInfo} for the analyzer used
	 * 			by the currently selected file or {@code null} if
	 * 			the analyzer is unknown or no file is selected
	 */
	public PlugInInfo getCurrentFileFormat() {
		FileInfo current = this.getSelectedFileInfo() ;
		if (current != null) {
			String formatId = current.getIdFormat() ;
			if (formatId != null) {
				return this.getAnalyzer(formatId) ;
			}// if
		}// if
		return null ;
	}// getCurrentFileFormat()

	/**
	 * Indicates if the format of the currently selected file is known.
	 * 
	 * @return	{@code true} if the currently selected file has
	 * 			a known analyzer, {@code false} otherwise
	 */
	public boolean currentFileFormatIsKnown() {
		return (this.getCurrentFileFormat() != null) ;
	}// currentFileFormatIsKnown()

	/**
	 * Removes the current selection from the history list and from
	 * the history file. If no file is currently selected, nothing
	 * is done.
	 */
	public void deleteCurrentSelection() {
		if (this.currentFileIndex != null) {
			this.deletedIndex = this.currentFileIndex.intValue() ;
			this.recentFiles.remove(this.deletedIndex) ;
			this.currentFileIndex = null ;
			this.writeHistory() ;
			this.fileRemoved = true ;
			try {
				this.notifyObservers() ;
			} catch (KameleonException ke) {
				this.displayDebugInformation(ke) ;
			}// try
			this.fileRemoved = false ;
		}// if
	}// deleteCurrentSelection()

	/**
	 * Indicates if a file has just been removed from the history.
	 * 
	 * @return	{@code true} if a file has just been removed from 
	 * 			the history, {@code false} otherwise
	 */
	public boolean fileRemoved() {
		return this.fileRemoved ;
	}// fileRemoved()

	/**
	 * Returns the index of the deleted file in the history.
	 * 
	 * @return	Index of the deleted file in the history
	 */
	public int getDeletedIndex() {
		return this.deletedIndex ;
	}// getDeletedIndex()

	/**
	 * Returns the names of all the available charsets.
	 * 
	 * @return	Array containing the names of all the available
	 * 			charsets
	 * 
	 * @see		Charset#availableCharsets()
	 */
	public static String[] getDefaultCharsets() {
		SortedMap<String, Charset> scs = Charset.availableCharsets() ;
		return scs.keySet().toArray(new String[scs.size()]) ;
	}// getDefaultCharsets()

	/**
	 * Sets the charset used to decode the analyzed files and to
	 * encode the generated files.
	 * 
	 * @param	charsetName
	 * 			name of the new charset
	 *///TODO Add exception if invalid charset ?
	public void setCharset(String charsetName) {
		if (Charset.isSupported(charsetName)) {
			this.charset = charsetName ;
		}// if
	}// setCharset(String)

	/**
	 * Returns the charset used to decode the analyzed files and to
	 * encode the generated files.
	 * 
	 * @return	Name of the charset used to encode and decode files
	 */
	public String getCharset() {
		return this.charset ;
	}// getCharset()

	/**
	 * Returns the file extension. The extension is considered to be any sequence of characters behind the
	 * {@code .} in the file name. If the given file is {@code null} or has no extension, {@code null} is returned.
	 * 
	 * @param 	file 
	 * 			file whose extension is requested
	 * 
	 * @return 	Extension of the given file or {@code null}
	 * 
	 * @see		#getExtension(String)
	 */
	public static String getExtension(File file) {
		return getExtension(file.getName()) ;
	}// getExtension(File)

	/**
	 * Returns the file extension. The extension is considered to be any sequence of characters behind the
	 * {@code .} in the file name. If the given file is {@code null} or has no extension, {@code null} is returned.
	 * 
	 * @param 	name 
	 * 			name or path of the file whose extension is requested
	 * 
	 * @return 	Extension of the given file or {@code null}
	 */
	public static String getExtension(String name) {
		if (name == null) return null ;
		int posDot = name.lastIndexOf('.') ;
		if (posDot == -1) return null ;
		return name.substring(posDot+1) ;
	}// getExtension(String)
	
	/**
	 * Indicates if at least one generator is selected.
	 * 
	 * @return	{@code true} if at least one generator is selected,
	 * 			{@code false} otherwise
	 */
	public boolean atLeastOneFormatSelected() {
		return !this.selectedGenerators.isEmpty() ;
	}// atLeastOneFormatSelected()

	/**
	 * Adds the given generator to the list of currently selected
	 * generators.
	 * 
	 * @param	formatId
	 * 			id of the added generator
	 */
	public void addSelectedFormat(String formatId) {
		if (!this.selectedGenerators.contains(formatId)) {
                        this.selectedGenerators.add(formatId) ;
                }// if
		try {
			this.notifyObservers() ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
	}// addSelectedFormat(String)

	/**
	 * Removes the given generator to the list of currently selected
	 * generators.
	 * 
	 * @param	formatId
	 * 			id of the removed generator
	 */
	public void removeSelectedFormat(String formatId) {
		this.selectedGenerators.remove(formatId) ;
		try {
			this.notifyObservers() ;
		} catch (KameleonException ke) {
			this.displayDebugInformation(ke) ;
		}// try
	}// removeSelectedFormat(String)

	/**
	 * Indicates whether is possible to make a generation. A generation
	 * is possible if:
	 * <ul>
	 * <li>a file is selected
	 * <li>the format of the selected file is selected
	 * <li>at least one generator is selected
	 * </ul>
	 * 
	 * @return	{@code true} if is possible to make a generation,
	 * 			{@code false} otherwise
	 */
	public boolean generationIsPossible() {
		return this.fileIsSelected() 
				&& this.currentFileFormatIsKnown()
				&& this.atLeastOneFormatSelected()  ;
	}// generationIsPossible()

	/**
	 * Returns all the ids of the selected generators. If no generator
	 * is selected, an empty list is returned.
	 * 
	 * @return	instance of {@code List<String>} with all the ids
	 * 			of the selected generators
	 */
	public List<String> getSelectedOutputFormats() {
		return this.selectedGenerators ;
	}// getSelectedOutputFormats()

}// class FileModel